<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8">
    <title>Sucha's Blog - Archive for November, 2020</title>
    <meta name="generator" content="MarkdownProjectCompositor.lua">
    <meta name="author" content="Sucha">
    <meta name="keywords" content="suchang, programming, Linux, Lua">
    <meta name="description" content="Sucha's blog">
    <link rel="shortcut icon" href="../images/ico.png">
    <link rel="stylesheet" type="text/css" href="../styles/blog.css">
    <link rel="stylesheet" type="text/css" href="../styles/prism.min.css">
    <style id="site_theme"></style>
  </head>
  <body>
    <div id="body">
      <div id="text">
	   <!-- Page published by cmark-gfm begins here --><h1>Sucha's Blog ~ Archive for November, 2020</h1>
<p><a id="p2"></a></p>
<div class="date">20年11月9日 周一 16:02</div>
<h2>JavaScript(1)</h2>
<p>学习了一下 javascript，使用的是 <a href="https://wangdoc.com/javascript/index.html">JavaScript 教程</a>，基础的 ES5，包含的内容可太丰富了。</p>
<p>跟其他语言的差别，主要在语法、面向对象，基础库和运行环境这几个部分。</p>
<h3>语法</h3>
<p>语法部分，'==' 可真够让人头疼的，为了比较会自动转换类型，所以教程建议直接使用 ‘===’ 进行严格比较。</p>
<p>相对高级的语言，都有 &lt;&lt; 和 &gt;&gt;，甚至 &gt;&gt;&gt; 操作符，可惜了，Lua 5.1 比如 LuaJIT 并没有这么好用的操作符，只能通过 bit 库来使用，或者塞给 C 代码处理吧。</p>
<p>JS 里面的 null 和 undefined 也得注意一下，它们几乎是一样的，除了 null 在 Number(null) 时为 0，而 Number(undefined) 为 NaN。</p>
<h3>面向对象</h3>
<p>这里一开始让我挺头疼的，比如原型对象及继承关系。</p>
<p>对比 C++/Java 的面向对象实现都隐藏在语言核心运行时里面，通过语法‘类’来实现继承，在类里面写构造函数，描述继承关系。且 C++ 因为是零成本抽象，极端强调运行时性能，继承、面向对象的逻辑在编译期几乎都解决了，只留下了动态派发给运行时处理。Java 有反射，任何时候都能拿到其继承关系，其所有的类都继承自 Object。两者作为静态语言，在运行时无法动态修改继承关系。</p>
<p>JS 虽然跟 Java 一样所有类都继承自 Object，但其继承的语言特性，是通过原型对象来实现的，运行时可以访问。原型对象就是类的 prototype 属性，构造函数则是 prototype.constructor，因为有 new 关键字的存在，定义一个新类，只需要定义构造函数就好了，比如</p>
<pre><code class="language-js">function Car(color) {
    this.color = color;
    console.log(&quot;color:&quot; + color);
}

var car = new Car(&quot;white&quot;);
</code></pre>
<p>会输出 'color:white&quot;。如果定义 Bus 继承 Car 是这么实现的：</p>
<pre><code class="language-js">function Bus(color) {
    Car.call(this, color);
}
Bus.prototype = Object.create(Car.prototype)
Bus.prototype.constructor = Bus

var bus = new Bus(&quot;red&quot;)
</code></pre>
<p>将输出 'color:red'，如果需要在运行时修改类的继承关系，propotype 和 prototype.constructor 必须同时修改。</p>
<p>感觉原型对象这个词，在继承关系上，跟 Objective-C 的类对象有一点像，不过实际表现及使用，千差万别。</p>
<p>对比 Lua 实现面向对象，感觉还是有一点像的，不过 Lua 里面实现面向对象，继承关系，需要了解的可太多了，需要了解 Lua 的元表（metatable）才好。</p>
<h3>基础库和运行环境</h3>
<p>作为一个动态脚本语言，JS 的基础库和运行环境挺丰富的，作为 Web 端统治性的语言，运行环境包含对 HTML DOM 的操作，以及浏览器环境，这两个部分我还没怎么看呢。</p>
<p>作为后端语言，基于 Node.js 事件驱动、非阻塞式 I/O 模型，极大降低了后端开发的难度，不过 Node 下的运行环境，我还没怎么接触。</p>
<p>虽然 JS 是单线程的 VM，但是 HTML 5 及 Node 都实践了其多线程交互的方式，也有了不同领域下的解决方案，其成熟程度，丰富程度，对比 Lua 可要好太多了。</p>
<p>引擎部分，标准桌面、后端用的是 Chrome V8，嵌入式应该用的是其他方案。现在已经有了前后兼容的 ES5、ES6 标准，貌似 ES7 也快要出来了。</p>
<p>--</p>
<p>总的来说，JS 是一门比较完善，还在不断渐进式发展，且考虑兼容性的编程语言，慢慢的将变成桌面应用编程语言，部分后端编程语言，可不再是脚本语言了。</p>
<p>相比 Go，JS 是动态类型，也不是静态编译的；相比 Lua，语言成熟度，库的丰富程度，应用范围都好好很多，没有 Lua 目的是宿主语言的累赘，虽然 Lua 还有速度上的优势。</p>
<p>而且，学会 JS 的好处可是，前端通吃，后端独立。</p>
<div class="category"><a href="CategoryProgramming.html">CategoryProgramming</a> / <a href="2020-11.html#p2">Permalink</a> / <a href="https://github.com/lalawue/homepage/discussions/categories/blog" target="_blank">Discussion</a></div>
<p><a id="p1"></a></p>
<div class="date">20年11月5日 周四 09:49</div>
<h2>go_bitcask</h2>
<p>学习了 Go 后要使用，实践的第一个程序是 <a href="https://github.com/lalawue/go_bitcask">go_bitcask</a>。其逻辑大部分源于 <a href="https://github.com/lalawue/ffi_bitcask.lua">ffi_bitcask.lua</a>。</p>
<p>但在 Go 这个版本中，因为一直开关读写文件效率比较低，所以做了一层抽象优化，将文件的读写抽象到了 DataFile 里面，其中，读的部分使用了 mmap，写的时候只保留一个活跃文件，一直追加写，也兼顾当前活跃文件的读，待到关闭数据库的时候才做 Sync()，并关闭文件。</p>
<p>在我的 16 年 MacBook Pro 版本 10.15.7 中，Go 1.13.4 读写文件极慢，LuaJIT FFI 版本用了不到 0.2 秒完成的测试，在 Go 这边需要 2 秒，太夸张了。</p>
<p>试了一下在云端 Linux 上面，效果好了很多，大概需要 0.2 秒，但 LuaJIT FFI 版本只需要 0.02 秒</p>
<p>LuaJIT FFI 版本的 bitcask</p>
<pre><code class="language-shell">$ time luajit test/test_bitcask.lua
PASS Bucket
PASS Set/Get
PASS Delete
PASS GC

real    0m0.021s
user    0m0.011s
sys     0m0.008s
</code></pre>
<p>Go 版本我编译后才测试的</p>
<pre><code class="language-shell">$ time ./test
using database: {Path:/tmp/go_bitcask BucketName:0 DataFileSize:512}
Pass Bucket
PASS Set/Get
PASS Delete
PASS GC

real    0m0.234s
user    0m0.004s
sys     0m0.030s
</code></pre>
<p>也许 Go 启动准备需要不少时间吧，或者是我实现的问题。</p>
<p>在 Go 版本中，我同样是使用了 struct map 到内存的方式来表示一条 record，然后一次性读取 key 及 value，再 slice 切割为 key 和 value。</p>
<p>映射的代码如下，感觉也没什么呀</p>
<pre><code class="language-go">// map recordinfo to bytes
func recordInfoToBytes(ri *recordInfo) []byte {
        var x reflect.SliceHeader
        x.Len = int(recordSize)
        x.Cap = int(recordSize)
        x.Data = uintptr(unsafe.Pointer(ri))
        retBytes := *(*[]byte)(unsafe.Pointer(&amp;ri))
        return retBytes[:recordSize]
}

// map byte to recordinfo
func bytesToRecordInfo(b []byte) *recordInfo {
        return (*recordInfo)(unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&amp;b)).Data))
}
</code></pre>
<p>也许完全运行起来以后，Go 的速度才会又继续上去吧，我 pprof 的结果，是 system call 占用时间最多，其中 Set 是占用很多的，其次是 GC，但是 GC 不常用，这个倒没什么关系。</p>
<p>Set 占用多，感觉说不过去呀，因为就检测如果有重复 key，做了 remove，然后再 set 新的 key/value，两次 system call write 函数。</p>
<p>也许 Go 版本真的应该做完整一些，将写的操作单独一个 goroutine，并且做 LSM 操作，将读写都 encode/decode 起来，再做一层抽象才好吧，但是目前这个算是简单版本，不打算再做这一层了。</p>
<div class="category"><a href="CategoryProgramming.html">CategoryProgramming</a> / <a href="2020-11.html#p1">Permalink</a> / <a href="https://github.com/lalawue/homepage/discussions/categories/blog" target="_blank">Discussion</a></div>
<p><a id="p0"></a></p>
<div class="date">20年11月4日 周三 10:32</div>
<h2>Go(1)</h2>
<p>又开始学习 Go，之前其实断断续续看过一些，因为没有实践过，也陆续淡忘了不少，现在重新拾起，教程用的是 <a href="https://github.com/Go-zh/tour">Go 语言官方教程中文版</a>以及另外一本（彻底贯彻学完就扔的习惯现在找不到网址了），后面因为反复查询标准库，用了很多 <a href="https://studygolang.com/pkgdoc">Golang 标准库文档</a>。</p>
<p>Go 是一门命令式的语言，入门曲线比较低，比如非面向对象，简单的控制流、少了继承、枚举等等，变量声明也简单明了；但是要掌握我觉得不算简单，比如 goroutine 以及 CSP 数据交换的并发模型，slice、反射等，因此学习起来有一个曲线。</p>
<p>Go 有不少必知必会的语言规定，比如变量名首字母大写才会在包里面导出，变量需要驼峰命名，带间隔的下划线是不被推荐的（VSCode 里面的 golints 有对我多次提示啦）。相比 C/C++、Java、Lua 等，我觉得变量声明、defer、slice、接口、并发模型、包管理系统，是 Go 的特色，可以拣出来说一说。</p>
<h3>变量声明</h3>
<p>这个知识点很简单了，可以不用指明变量类型，让系统推断，比如</p>
<pre><code class="language-go">a := 10
</code></pre>
<p>或者指明类型</p>
<pre><code class="language-go">var a uint32 = 10
</code></pre>
<p>上面的两种是有区别的，第一种 Go 默认是 int 类型，下面指定了 uint32 类型，Go 是强类型语言，变量的后续使用必须对齐类型，否则编译报错。当然可以这样写 a := uint32(10)，成功用第一种方式声明了 uint32 类型。类型转换还有下面这样的</p>
<pre><code class="language-go">var b interface{} = 10
var c interface{} = &quot;hello&quot;
var d string = c.(string)
fmt.Println(b, c, d)
</code></pre>
<p>可以看到，虽然 Go 没有 C/C++ 的 void*，但可以用万能的接口 interface{} 弥补，interface{} 还可以在运行期用来做类型推断，这里不细说了。</p>
<p>Go 里面方便的地方，if 控制块一开始就可以定义新的变量在控制块内使用，比如下面判断文件是否打开成功的例子：</p>
<pre><code class="language-go">if fp, err := os.Open(&quot;/path/to/file&quot;); err != nil {
    defer fp.Close()
    // ...
} else {
    return err
}
</code></pre>
<p>上面的 fp、err 只能在 if 及 else 块内使用，出了 if、else 的控制范围，编译会报错。</p>
<p>以上，方便的变量定义，算是 go 的特色了，其方便程度，跟 Lua 有点像，但是 Lua 默认是全局可见的，Go 有控制块的限定，包内控制块之外必须首字母大写才能导出。对于大型系统，我觉得 Go 的定义更稳妥一些。</p>
<h3>defer</h3>
<p>上面的例子已经提前用了 defer，比如在文件打开成功后，defer 了一个关闭函数，当 if 语句所在函数返回后，defer 按照先进后出的原则退栈运行。</p>
<p>defer 运行的只能是函数，所以如果是一些变量需要离开函数后保存，可以下面这样</p>
<pre><code class="language-go">stepRecorder = &quot;enter function&quot;
defer func(){
    stepRecorder = &quot;leave function&quot;
}()
// ...
</code></pre>
<p>defer 写得太爽了之后，遇到了习惯的反面，比如在 for 循环里面打开了文件，希望下个循环关闭，就老想着能够有这样一个关键字，在当前循环结束前运行这个关键字后面带的函数，因为在复杂的 for 里面，有 return，还有 continue，还有正常的流程。</p>
<p>然而 Go 没有这样的关键字，所以很遗憾的老老实实按老路子写代码了。</p>
<h3>slice</h3>
<p>Go 里面 array 跟 slice 有紧密的联系，我总感觉 array 是 slice 的特例，是一个预定义的 slice，举个例子：</p>
<pre><code class="language-go">a := []int{1, 2, 3, 4}
b := a[:2]
fmt.Println(a, b)
</code></pre>
<p>打印出来的是下面这样的</p>
<pre><code class="language-go">[1 2 3 4] [1 2]
</code></pre>
<p>可以看到其实两者底层是一致的，只不过 array 固定了 slice 的 len 和 cap，slice 的切割是左闭右开的区间 [)，类似 for 定义的 for i := 0; i &lt; 5; i++ 的一个语言习惯吧。</p>
<p>slice 可以使用 append 扩容，比如</p>
<pre><code class="language-go">b = append(a, 5)
</code></pre>
<p>结果是输出</p>
<pre><code class="language-go">[1 2 3 4 5]
</code></pre>
<p>slice 使用 make([]int, 5) 等方式定义，虽然用着像数组，但实际上可变长度的设计跟 C++ 里面的 vector 有点像了，由于 Go 没有 C++ STL 的 vector，slice 就是这样大量使用了。</p>
<p>不过我总觉得，虽然底层的抽象是这样，但是用着确实不方便，使用者得很熟悉底层的抽象、分割才行。</p>
<h3>接口</h3>
<p>Go 没有继承，没有 Java 这样 Object 的基类，也就没有面向对象的基础。其语言设计者认为面向对象不是必要的，特别是深层级的继承，过于复杂了。其推荐通过接口抽象，所有实现了接口定义函数的 struct 类型，认为具有接口的功能，是接口抽象的代表。在实现上 struct 没有特别的关键字来声明实现了某些接口，编译期、运行时核心是知道的，提前匹配了。</p>
<p>比如我们定义一个接口，并写某个 struct 实现接口</p>
<pre><code class="language-go">type Dog interface {
    Name() string
}

type BullDog struct {}
func (dog *BullDog) Name() string {
    return &quot;BullDog&quot;
}

var dog Dog = nil
dog = &amp;BullDog{}
fmt.Println(&quot;dog is&quot;, dog.Name())
</code></pre>
<p>以上，斗牛犬 BullDog 实现了 Dog 的 Name() 接口，没有声明实现的关键字，只需要实际实现了这个接口，就可以编译运行通过。</p>
<p>通过上面的定义，对比 Java 的接口、抽象基类定义，可以想像，由于 Go 没有继承，以及 interface 的默认实现或者说抽象基类的东西，也没有函数重载，如果在 Go 里面定义了复杂的接口，或者某些特性通过接口暴露但只有很少量的实现需要特别处理，都是一件头疼的事情吧。</p>
<p>不过相反的，在稍微大型一点的代码里，应该可以新定义调整一个接口来区分不同的类型实现吧。</p>
<h3>并发模型</h3>
<p>Go 的并发模型是 goroutine，定义了函数后，通过命令字 go 来驱动，其通讯模式，用的是 CSP，通过管道（channel）通讯，而不鼓励通过共享变量通讯。</p>
<p>C 抽象了体系的基本寄存器，C++ 是个牛刀，多模式的语言，但在早期的语言规范里面，没有多线程的定义，我查了一下，直到 C++11 才有语言提供的多线程，Java 的多线程语言内置支持，但是 C/C++/Java 的多线程实现，可以看成都是 pthread 的封装，一个语言线程，对应操作系统的一个线程。</p>
<p>但在 Go 里面不是这样的，Go 的线程独立于操作系统的线程，是协程，但调度是 Go 核心在用户空间实现的，虽然用起来跟线程类似。goroutine 的通讯、锁，官方的例子都是建议通过 channel 传递数据以及控制，但实际上不是什么都适合在 channel 中传递，有时候传递行为的控制反而是复杂的，所以也是有锁的，得合理区分场景使用，且这个时候的锁也是得考虑死锁的。</p>
<p>由于没有很深入的使用，这里就不展开了（没实料了）。</p>
<p>对比一下 Lua 的协程，Lua 是单线程模型，因为 Lua 语言本身的设计就是一个宿主语言，最开始的设计是靠着 C，而 C 的语言核心没有定义线程的，这也导致了 Lua 协程的设计没有考虑多线程。网上 Lua 多线程的模式，其实就是 C 多线程的模式，一个 C pthread 线程，带一个 Lua VM，通过 CSP 或者共享内存通讯。</p>
<p>而 Go 直接抽象了协程、CSP 这套协程、多线程通讯机制。</p>
<h3>包管理系统</h3>
<p>Go 的包管理系统我有看过浅显的资料，区分不同发展阶段，有使用 vendor 目录带支持包，或者新版本 go mod 的阶段共享支持包。</p>
<p>Go 使用关键字 package 定义包，包里面首字母大写导出函数或者变量，这是语言的强制约定，减少了关键字，另外 main 函数是打包编译为二进制后的运行入口。</p>
<p>C/C++ 是没有明确包管理的，只有系统层级的二进制库，以及编译、链接顺序。理好 include，然后依赖不同的 build tool 来构建，比如大名鼎鼎的 make，或者 Nijia，一般不会生成静态链接的二进制，当然也可以生成，只是真的很少见，由于可以控制编译期的任何一个阶段输出，在嵌入式应用可好了，输出动态链接，还可以各种裁剪，但相反就是要求高了不少，得自己学会搞定编译运行期的所有问题。</p>
<p>Java 语言的定义也是不带包管理工具的，但工程实践是有 mavel 或者 gradle，我不熟悉，就不展开了。</p>
<p>Lua 也没有包管理，不过默认的操作系统文件夹，都可以是一个包层级，然后命令行可以指定在哪里寻找包，以及动态库。</p>
<p>话说 Rust 也有包管理，但我没有深入具体跟仓库是怎么结合的。</p>
<p>我只用过 go mod 的包管理，go mod 命令行初始化后，感觉后面就很简单了。再说 IDE 也是强，VSCode 配置好后，任何不需要的包，都无法导入，而实际用了标准库的包，IDE 都自动帮忙导入到 import 里面。</p>
<p>感觉强在 github 等依赖外部仓库的包，go mod 看到有用到，运行 tidy 命令后，可以帮忙直接下载，这里并不依赖中央仓库的，算是一个特色，毕竟类似 CocoaPod 也是有中央仓库的，或者起码得指定私有仓库的地址才行。</p>
<p>编译成静态链接的的好处，是可以无视系统版本了，对于大型的程序，增大的这点空间不算什么，毕竟对比大量异构部署来说，极大减轻了部署的困难，俗称统一交付。估计谷歌内部对于 C/C++ 可以输出静态链接的版本，早就这么做了，估计不这么做的话，那么多个二进制输出版本，debug 也有困难吧。</p>
<p>从包管理，可以看到交付的逻辑，当然也注定了 Go 不会应用到嵌入式里面。除非能出一个非静态链接的版本，但是 Go 的核心运行时，能够单独出供动态链接的二进制版本吗，感觉往这个方向的估计都没考虑了。</p>
<p>有脚手架的语言，Rust、Go 是很方便的，有统一的脚手架，也规范了交付的正统逻辑。</p>
<p>--</p>
<p>用 Go 写了一点小东西，所以有了上面的一些思考，可惜是还没有用上 goroutine，那就先输出这么多吧。</p>
<p>最后说一下内存占用，C/C++ &lt;- Lua &lt;- Go &lt;- Java，Lua 不是正统的非宿主语言，这里有加入 VM 对比的考虑，一般的评测，都是认为 Go 是要比 Java 快的，再说 JVM 基本占用内存就很大了，如果不是大型程序，用 JVM 的成本会比较高。</p>
<p>但是 Java 多年来的基础建设都很好，不管是 JVM 还是 IDE，我用过最好的 IDE 就是 Java 的，太省心了。但是另一方面，Java 的交付、部署应该是没有 Go 的简单，至少也是有 JVM 版本的，而 Go 真的就是统一交付了。</p>
<p>Go 席卷了互联网，Docker 及云的基础建设就是明证，Java 瓜分了剩下的很大一块，接着是 Python 的简易运维，C++ 是核心中的核心，不晓得这个分布是不是当前占有率的反映。</p>
<div class="category"><a href="CategoryProgramming.html">CategoryProgramming</a> / <a href="2020-11.html#p0">Permalink</a> / <a href="https://github.com/lalawue/homepage/discussions/categories/blog" target="_blank">Discussion</a></div>
<!-- Page published by cmark-gfm ends here -->
  <div id="foot">2004-<script>var d = new
	Date();document.write(d.getFullYear())</script> &copy;
	Sucha. Powered by MarkdownProjectCompositor.
  </div>
  </div><!-- text -->
  <div id="sidebar">
  </div><!-- sidebar -->
  <script src="../js/prism.min.js" async="async"></script>
  <script src="../js/blog_sidebar.js"></script>
  </div> <!-- body -->
</body>
</html>